/* --------------------------------------------------------
 * Copyright (c) aeky , Inc.  All rights reserved.
 * --------------------------------------------------------
 */
package com.aeky.server.support;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletConfig;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.ServletConfigAware;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;

import com.google.gwt.user.client.rpc.RemoteService;

/**
 * GWT Handler Class.
 * 
 * @author Shawn
 */
public class GWTHandler extends AbstractUrlHandlerMapping implements HandlerMapping, InitializingBean,
        ServletContextAware, ServletConfigAware {

    private static final Logger LOG = LoggerFactory.getLogger(GWTHandler.class);

    /** The mapping. */
    private Map<String, Object> mapping = new HashMap<String, Object>();

    /** The factory. */
    private RPCServiceExporterFactory factory;

    /** The disable response caching. */
    private boolean disableResponseCaching = false;

    /** The throw undeclared exception to servlet container. */
    private boolean throwUndeclaredExceptionToServletContainer = false;

    /** The scan parent application context. */
    private boolean scanParentApplicationContext = false;

    /** The servlet config. */
    private ServletConfig servletConfig;

    /**
     * Scans the application context and its parents for service beans that
     * implement the {@link GWTRequestMapping}.
     * 
     * @param appContext Application context
     */
    private void scanForAnnotatedBeans(final ApplicationContext appContext) {
        if (appContext == null) {
            return;
        }
        for (String beanName : appContext.getBeanNamesForType(RemoteService.class)) {
            Object service = appContext.getBean(beanName);
            if (service == null) {
                continue;
            }
            final Class<?> beanClass = service.getClass();

            final GWTRequestMapping requestMapping = ReflectionUtils.findAnnotation(beanClass, GWTRequestMapping.class);
            if (requestMapping == null) {
                continue;
            }

            // Create serviceExporter to bind to
            String mapping = requestMapping.value();
            if (getMappings().containsKey(mapping)) {
                LOG.warn("Bean '" + mapping + "' already in mapping, skipping.");
            } else {
                getMappings().put(mapping, service);
            }
        }
        if (scanParentApplicationContext) {
            scanForAnnotatedBeans(appContext.getParent());
        }
    }

    /**
     * Recursively scan the parent application contexts for annotated beans to
     * publish. Beans from applications contexts that are lower in the hierarchy
     * overwrite beans found in parent application contexts.
     * 
     * @param scanParentApplicationContext Defaults to <code>false</code>
     */
    public void setScanParentApplicationContext(boolean scanParentApplicationContext) {
        this.scanParentApplicationContext = scanParentApplicationContext;
    }

    /**
     * Initialize service instance.
     * 
     * @param exporter exporter
     * @param service service
     * @param serviceInterfaces serviceInterfaces
     * 
     * @return RPCServiceExporter
     */
    private RPCServiceExporter initServiceInstance(RPCServiceExporter exporter, Object service,
            Class<RemoteService>[] serviceInterfaces) {
        try {
            exporter.setResponseCachingDisabled(disableResponseCaching);
            exporter.setServletContext(getServletContext());
            exporter.setServletConfig(servletConfig);
            exporter.setService(service);
            exporter.setServiceInterfaces(serviceInterfaces);
            exporter.setThrowUndeclaredExceptionToServletContainer(throwUndeclaredExceptionToServletContainer);
            exporter.afterPropertiesSet();
            return exporter;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Get Service mapping.
     * 
     * @return Map
     */
    protected Map<String, Object> getMappings() {
        return mapping;
    }

    /**
     * Set a mapping between URLs and services.
     * 
     * @param mapping mapping
     */
    public void setMappings(Map<String, Object> mapping) {
        this.mapping = mapping;
    }

    /**
     * Invoked automatically by Spring after initialisation.
     * 
     * @throws Exception exception
     */
    public void afterPropertiesSet() throws Exception {
        if (factory == null) {
            factory = new DefaultRPCServiceExporterFactory();
        }
        scanForAnnotatedBeans(getApplicationContext());
        for (Map.Entry<String, Object> entry : mapping.entrySet()) {
            RPCServiceExporter exporter = factory.create();
            registerHandler(
                    entry.getKey(),
                    initServiceInstance(exporter, entry.getValue(),
                            ReflectionUtils.getExposedInterfaces(entry.getValue().getClass())));
        }
        this.mapping = null;
    }

    /**
     * Optionally, a {@link RPCServiceExporterFactory} can be injected if a
     * different implementation or setup is required. Note that after
     * initialization, the following sequence of invocations will be performed
     * on the {@code serviceExporter} :<br>
     * <br>
     * <code>
     * exporter.setServletContext();<br>
     * exporter.setService();<br>
     * exporter.setServiceInterfaces();<br>
     * exporter.afterPropertiesSet();<br>
     * </code>
     * 
     * @param factory factory
     */
    public void setServiceExporterFactory(RPCServiceExporterFactory factory) {
        this.factory = factory;
    }

    /**
     * Can be used to explicitly disable caching of RPC responses in the client
     * by modifying the HTTP headers of the response.
     * 
     * @param disableResponseCaching disableResponseCaching
     */
    public void setDisableResponseCaching(boolean disableResponseCaching) {
        this.disableResponseCaching = disableResponseCaching;
    }

    /**
     * Sets the throw undeclared exception to servlet container.
     * 
     * @param throwUndeclaredExceptionToServletContainer
     *            throwUndeclaredExceptionToServletContainer
     * 
     * @see {@link RPCServiceExporter#setThrowUndeclaredExceptionToServletContainer(boolean)}
     */
    public void setThrowUndeclaredExceptionToServletContainer(boolean throwUndeclaredExceptionToServletContainer) {
        this.throwUndeclaredExceptionToServletContainer = throwUndeclaredExceptionToServletContainer;
    }

    /**
     * Setter for servlet configuration.
     * 
     * @param servletConfig servletConfig
     */
    public void setServletConfig(ServletConfig servletConfig) {
        this.servletConfig = servletConfig;
    }

}
